package ig.finanzas;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;

import ig.util.excepciones.DatoInvalidoExcepcion;
import ig.util.excepciones.LogicaExcepcion;

/**
 * Esta clase implementa los calculos de unas cuantas funciones y operaciones financieras.  
 * @author lfgarcia
 */
public class LibreriaOperacionesFinancieras {

    /**
     * Calcula el valor de una curva cero cupon con una funcion de 4 betas.
     * @param b0
     * @param b1
     * @param b2
     * @param b3
     * @param numdias
     * @return
     * @throws LogicaExcepcion Si el resultado es imaginario.
     * @throws DatoInvalidoExcepcion Si beta4 es cero o el numdias es 0.
     */
    public static double valorTasaCurva(double b0, double b1, double b2,
            double b3, int numdias) throws LogicaExcepcion, DatoInvalidoExcepcion {

        if (b3 == 0) {
            throw new DatoInvalidoExcepcion("El valor de beta3 fue 0.");
        }
        if (numdias == 0) {
            throw new DatoInvalidoExcepcion("El numero de dias especificado fue 0.");
        }
        // Se modifica el calulo de la tasa segun formula de betas revisada con Ivan 21/06/2011 JChuquen
        double fraccionpos = ((double) numdias / 365) / b3;
        double fraccion = -fraccionpos;
        double exponencial = Math.exp(fraccion);
        double componente1 = b0 / 100;
        double componente2 = (((b1/100) + (b2/100))*(1-exponencial))/fraccionpos;
        double componente3 = (b2/100)*exponencial;
        double sumacomponentes = componente1 + componente2 - componente3;

        //double resultado = Math.exp(b0 / 100 + ((b1 / 100 + b2 / 100) * (1 - exponencial) / fraccion) - ((b2 / 100) * exponencial)) - 1;
        double resultado = Math.exp(sumacomponentes) - 1;

        if (new Double(resultado).isNaN()) {
            throw new LogicaExcepcion("El resultado obtenido es un numero imaginario.");
        }
        return resultado;
    }

    /**
     * Calcula la Tasa Interna de Retorno (TIR) a una serie de flujos. Si el resultado de la tir
     * es mayor que 10 (tir = 1000%) o menor que -10 (tir=-1000) el valor que devuelve la funcion es
     * de 10 y -10 respectivamente.
     * @param flujos
     * @return
     * @throws DatoInvalidoExcepcion Si la coleccion de flujos no posee valores.
     * @throws LogicaExcepcion Si no se tienen suficientes valores para el calculo.
     */
    public static double tir(Collection flujos) throws DatoInvalidoExcepcion, LogicaExcepcion {

        if (flujos == null) {
            throw new DatoInvalidoExcepcion("La coleccion de flujos especificada es nula.");
        }
        if (flujos.size() == 0) {
            throw new DatoInvalidoExcepcion("La coleccion de flujos especificada se encuentra vacia");
        }
        if (flujos.size() == 1) {
            throw new LogicaExcepcion("Insuficientes datos para el calculo de la tir");
        }
        
        double tirdeprueba = 0.0;
        double incremento = 0.6;
        double vpestimado = 0;
        double vpviejo = 0;
        boolean vppositivo;
        double iteraciones = 0;
        boolean seaumento = true;

        ArrayList flujosOrdenados = new ArrayList(flujos);
        Collections.sort(flujosOrdenados);
        Date fechaAnalisis = ((Flujo) flujosOrdenados.get(0)).getFecha();

        try {
            vpestimado = valorarFlujos(tirdeprueba, 365, fechaAnalisis, flujos);
            vpviejo = vpestimado;
        } catch (DatoInvalidoExcepcion e) {
        }

        if (vpestimado >= 0) {
            vppositivo = true;
        } else {
            vppositivo = false;
        }
        while ((vpestimado > 0.000001) || (vpestimado < -0.000001)) {
            incremento = incremento / 1.3;
            iteraciones += 1;
            if ((vppositivo && vpestimado > 0) || (!vppositivo && vpestimado > 0)) {
                if ((vpestimado > vpviejo && seaumento) || (vpestimado < vpviejo && !seaumento)) {
                    tirdeprueba -= (incremento * 4);
                    seaumento = false;

                } else {
                    tirdeprueba += incremento;
                    seaumento = true;
                }
            } else {
                if ((vpestimado > vpviejo && seaumento) || (vpestimado < vpviejo && !seaumento)) {
                    tirdeprueba += (incremento * 4);
                    seaumento = true;
                } else {
                    tirdeprueba -= incremento;
                    seaumento = false;
                }
            }
            try {
                vpviejo = vpestimado;
                vpestimado = valorarFlujos(tirdeprueba, 365, fechaAnalisis, flujos);

            } catch (DatoInvalidoExcepcion e1) {
            }

            if (tirdeprueba > 10) {
                return 10;
            } else if (tirdeprueba < -10) {
                return -10;
            }
            if (iteraciones > 1000) {
                return tirdeprueba;                      
            }
        }
        return tirdeprueba;   
    }

    private static double valorarFlujos(double interes, int diasano, Date fechaAnalisis, Collection flujos) throws DatoInvalidoExcepcion {
        double total = 0;
        Iterator i = flujos.iterator();
        while (i.hasNext()) {
            Flujo flujo = (Flujo) i.next();
            total += flujo.valorDescontado(interes, diasano, fechaAnalisis);
        }

        return total;
    }

    /**
     * Descuenta un monto a un numero de dias y a una tasa especificada.
     * @param monto
     * @param num
     * @param interes
     * @param diasano
     * @return
     * @throws DatoInvalidoExcepcion Si el valor de la tasa de descuento es igual o menor a -1.
     */
    public static double valorDescontando(double monto, long num, double interes, int diasano) throws DatoInvalidoExcepcion {

        if (interes <= -1) {
            interes = -0.99;
//			throw new DatoInvalidoExcepcion("El valor de la tasa de interes es menor o igual a -1.");
        }
        return (double) monto / Math.pow(((double) interes + 1), (double) num / diasano);
    }

    /**
     * Convierte el valor de una tasa de una expresion o modalidad a otra.
     * @param valor
     * @param expresionvalor
     * @param expresion
     * @return
     * @throws DatoInvalidoExcepcion Si alguna de las expresiones es nula.
     */
    public static double conversionTasas(double valor, Modalidad expresionvalor, Modalidad expresion) throws DatoInvalidoExcepcion {

        if (expresionvalor == null) {
            throw new DatoInvalidoExcepcion("La modalidad en que viene expresada el valor es nula.");
        }
        if (expresion == null) {
            throw new DatoInvalidoExcepcion("La modalidad a la que se debe convertir el valor es nula.");
        }
        double tasaefectiva = 0;
        if (expresionvalor.esNominal()) {
            //Llevarla a efectiva
            tasaefectiva = valor / (360 / expresionvalor.getNumDiasPeriodo());
        } else {
            tasaefectiva = valor;
        }
        double tasaefectivamodexp2 = 0;
        if (expresionvalor.getModo() == Modalidad.MODO_ANTICIPADO && expresion.getModo() == Modalidad.MODO_VENCIDO) {
            tasaefectivamodexp2 = (1 / (1 - tasaefectiva)) - 1;
        } else if (expresionvalor.getModo() == Modalidad.MODO_VENCIDO && expresion.getModo() == Modalidad.MODO_ANTICIPADO) {
            tasaefectivamodexp2 = tasaefectiva / (1 + tasaefectiva);
        } else {
            tasaefectivamodexp2 = tasaefectiva;
        }
        double tasaefectivamodperexp2 = 0;
        if (expresionvalor.getPeriodo() != expresion.getPeriodo() && expresion.getModo() == Modalidad.MODO_VENCIDO) {
            tasaefectivamodperexp2 = Math.pow((1 + tasaefectivamodexp2), ((double) expresion.getNumDiasPeriodo() / (double) expresionvalor.getNumDiasPeriodo())) - 1;
        } else if (expresionvalor.getPeriodo() != expresion.getPeriodo() && expresion.getModo() == Modalidad.MODO_ANTICIPADO) {
            tasaefectivamodperexp2 = 1 - Math.pow((1 - tasaefectivamodexp2), ((double) expresion.getNumDiasPeriodo() / (double) expresionvalor.getNumDiasPeriodo()));
        } else {
            tasaefectivamodperexp2 = tasaefectivamodexp2;
        }
        if (expresion.esNominal()) {
            return tasaefectivamodperexp2 * 360 / expresion.getNumDiasPeriodo();
        }
        return tasaefectivamodperexp2;
    }

    /**
     * Interpolacion de Pocisiones.
     * Creado por Cristhiam Reina
     * Creado el 18/07/2011
     * @param pocisiones
     * @param dias
     * @return
     * @throws DatoInvalidoExcepcion Si alguna de las expresiones es nula.
     */

    public static double obtenerValorVigentePlazos(double[][] pocisiones, int dias)
            throws LogicaExcepcion, DatoInvalidoExcepcion{

        int n_pos = pocisiones.length;

        if (pocisiones.length == 0)
            throw new DatoInvalidoExcepcion("No llego ninguna pocision para realizar la interpolacion.");

        if (dias == 0)
            throw new DatoInvalidoExcepcion("El numero de dias especificado fue 0.");


        if(n_pos == 1)
            return pocisiones[0][1];
        else{
            if(dias <= pocisiones[0][0])
                return pocisiones[0][1];
            else if (dias >= pocisiones[n_pos-1][0])
                return pocisiones[n_pos-1][1];
            else{

                double plazo_arriba  = 0;
                double plazo_abajo   = 0;
                double tasa_arriba   = 0;
                double tasa_abajo    = 0;


                for(int i = 0; i<(n_pos-1);i++ ){

                    if(dias == pocisiones[i][0]){
                        return pocisiones[i][1];
                    }
                    else if(dias < pocisiones[i][0]){
                        plazo_arriba    = pocisiones[i][0];
                        tasa_arriba     = pocisiones[i][1];

                        plazo_abajo     = pocisiones[i-1][0];
                        tasa_abajo      = pocisiones[i-1][1];
                        break;
                    }
                }

                double val_abs_arr =  Math.abs(dias - plazo_arriba);
                double val_abs_aba =  Math.abs(dias - plazo_abajo);

                return ((val_abs_arr * tasa_abajo) + (val_abs_aba * tasa_arriba)) / Math.abs(plazo_arriba - plazo_abajo);

            }
        }


    }
}
